-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/kob 66 angebote stadtweit #24
base: dev
Are you sure you want to change the base?
Conversation
# Conflicts: # frontend/src/core/core.router.ts # frontend/src/features/admin/features/the-contact-points/api/ContactPointsManipulationClient.ts # frontend/src/features/admin/features/the-contact-points/components/SaveUpdateButton.vue # frontend/src/features/admin/features/the-contact-points/middelware/useContactPoints.ts # frontend/src/features/admin/features/the-contact-points/the-contact-points-overview.vue # frontend/src/features/admin/the-admin-overview.vue # frontend/src/features/the-app-bar/features/the-drawer-main/the-drawer-main-store.module.ts # frontend/src/features/the-main/the-main.vue # frontend/src/features/the-unterstuetzungsfinder/features/the-contact-points/the-contact-points.routes.ts # frontend/src/features/the-unterstuetzungsfinder/features/the-contact-points/the-contact-points.vue
frontend/src/features/admin/features/the-additional/features/the-downloads/the-downloads.vue
Outdated
Show resolved
Hide resolved
frontend/src/features/admin/features/the-offers/api/OffersManipulationClient.ts
Outdated
Show resolved
Hide resolved
frontend/src/features/admin/features/the-offers/api/OffersManipulationClient.ts
Show resolved
Hide resolved
# Conflicts: # frontend/src/features/the-unterstuetzungsfinder/features/the-contact-points/the-contact-points.routes.ts
# Conflicts: # frontend/package-lock.json
@coderabbitai full review |
WalkthroughThe changes in this pull request involve significant updates to the frontend codebase, primarily focusing on routing, state management, component enhancements, and the introduction of new features related to offers. Key modifications include renaming and restructuring routes, the removal of the "Angebote" module, and the addition of new components for managing offers. Various components have been updated for improved styling, and several new API functions have been introduced for handling offers. The overall structure and functionality of many components remain intact, with enhancements aimed at consistency and clarity. Changes
Poem
Warning Rate limit exceeded@ThomasAFink has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 25 minutes and 45 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Caution
Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.
🛑 Comments failed to post (67)
frontend/src/features/commons/mixins/formatter.mixin.ts (1)
8-8: 💡 Codebase verification
Type mismatch detected in formatDate usage
The test file shows that
formatDate
is being called with aDate
object (new Date("2021-12-31")
), but the method signature expects a string parameter. This is a breaking change that needs to be addressed.
frontend/src/features/commons/mixins/formatter.spec.ts
: Test passes aDate
object but method expects string- Current implementation uses
moment(date)
which accepts both string and Date objects, suggesting the type restriction might be unnecessary🔗 Analysis chain
Verify all callers pass string dates.
The parameter type change from
Date
tostring
could cause type errors in existing code.Let's verify all usages of the formatDate method:
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Find all calls to formatDate to verify type compatibility # Search for components importing the formatter mixin echo "Components using formatter mixin:" rg -l "formatter\.mixin" # Search for formatDate method calls echo -e "\nformatDate method calls:" rg "formatDate\(" -A 2Length of output: 770
Script:
#!/bin/bash # Let's check the test file to understand the actual type being passed cat frontend/src/features/commons/mixins/formatter.spec.tsLength of output: 934
frontend/src/features/commons/api/OfferGetClient.ts (1)
8-10:
⚠️ Potential issueAdd input validation and use URL template literals.
The current implementation has potential security implications:
- Direct string concatenation in URLs can be risky
- No validation of the id parameter
Apply these improvements:
+const isValidOfferId = (id: string): boolean => { + return /^[a-zA-Z0-9-_]+$/.test(id); +}; export const getOfferById = (id: string) => { + if (!id || !isValidOfferId(id)) { + throw new Error('Invalid offer ID'); + } - return httpGetJson<Offer>("/offers/" + id); + return httpGetJson<Offer>(`/offers/${id}`); };📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.const isValidOfferId = (id: string): boolean => { return /^[a-zA-Z0-9-_]+$/.test(id); }; export const getOfferById = (id: string) => { if (!id || !isValidOfferId(id)) { throw new Error('Invalid offer ID'); } return httpGetJson<Offer>(`/offers/${id}`); };
frontend/src/features/commons/types/Offer.ts (2)
12-20: 🛠️ Refactor suggestion
Refactor interface hierarchy to reduce duplication
The
OfferListItem
interface largely duplicates theOffer
interface with only one difference in thedepartments
property. This could lead to maintenance issues.Consider using interface extension or utility types:
-export interface OfferListItem { - id?: string; - title: string; - description: string; - startDate?: string; - endDate?: string; - imageLink: string; - departments: string[]; -} +export interface OfferListItem extends Omit<Offer, 'departments'> { + departments: string[]; // Required in list items +}Committable suggestion skipped: line range outside the PR's diff.
1-20: 💡 Codebase verification
Consider using existing Department enum and add documentation
Based on the codebase analysis, there's an existing
Department
enum infrontend/src/features/the-unterstuetzungsfinder/features/the-contact-points/types/department.type.ts
that defines valid department values (RIT, POR, DIR, NONE). The interfaces are being used in multiple places including offer filtering based on user departments.
- Consider reusing the existing
Department
enum for type safety- Add JSDoc documentation to explain the difference between optional and required departments in the interfaces
- Ensure consistency with the department values used in
AdminUserInfo
Example:
import { Department } from '../the-unterstuetzungsfinder/features/the-contact-points/types/department.type'; /** * Represents an offer entity for creation/editing. * Departments are optional during draft/creation. */ export interface Offer { id?: string; title: string; description: string; startDate?: string; endDate?: string; imageLink: string; departments?: Department[]; } /** * Represents an offer in list views. * Departments are required for filtering by user's department. */ export interface OfferListItem extends Omit<Offer, 'departments'> { departments: Department[]; }🔗 Analysis chain
Add documentation and consider type safety for departments
The relationship between
Offer
andOfferListItem
and their usage patterns aren't immediately clear. Additionally, thedepartments
field could benefit from stronger typing.Consider these improvements:
- Add JSDoc documentation explaining the purpose and usage of each interface
- Use an enum or union type for departments
- Consider using a discriminated union if these represent different states of an offer
Example:
/** Valid department values for offers */ export enum Department { HR = 'HR', IT = 'IT', // ... add other valid departments } /** * Represents a complete offer entity. * Used when creating or editing individual offers. */ export interface Offer { // ... existing fields departments?: Department[]; } /** * Represents an offer in a list view. * Requires departments to enable filtering and grouping in list displays. */ export interface OfferListItem extends Omit<Offer, 'departments'> { departments: Department[]; }Let's verify the current usage of departments:
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Search for department values to understand valid options rg -A 1 "departments.*=" --type typescriptLength of output: 79
Script:
#!/bin/bash # Search for department values using ripgrep without file type filter rg -A 1 "departments.*=" # Also search for any usage of these interfaces rg -l "interface (Offer|OfferListItem)" # Search for department-related string literals rg "'[A-Z][A-Za-z]+'" --type tsLength of output: 372
Script:
#!/bin/bash # Search for any usage of departments in the codebase rg -A 3 "departments" --type-add 'ts:*.{ts,tsx}' --type ts # Search for files importing or using these interfaces rg -A 2 "import.*Offer(ListItem)?.*from" --type-add 'ts:*.{ts,tsx}' --type tsLength of output: 5199
Script:
#!/bin/bash # Search for department-related code in admin user info rg -A 3 "department" frontend/src/features/admin/components/userinformation/ # Look for any enum or type definitions related to departments ast-grep --pattern 'enum $_Department { $$$ }' # Search for department string literals in the codebase rg '"[A-Z][A-Za-z]+"' --type-add 'ts:*.{ts,tsx}' --type tsLength of output: 7097
frontend/src/core/services/downloads/privacypolicy.vue (1)
4-8:
⚠️ Potential issueValidate download URLs for security
The download URL is used directly from the backend response without validation, which could be a security risk if the backend is compromised.
Consider adding URL validation:
<script lang="ts"> import {defineComponent} from "vue"; import {PageType} from "@/features/the-additional/common/model/PageType"; import {useGetAdditionalContent} from "@/features/the-additional/common/middleware/AdditionalPageService"; + +// Allowed domains for downloads +const ALLOWED_DOMAINS = ['your-domain.com']; + +function isValidDownloadUrl(url: string): boolean { + try { + const parsedUrl = new URL(url); + return ALLOWED_DOMAINS.some(domain => parsedUrl.hostname.endsWith(domain)); + } catch { + return false; + } +} export default defineComponent ({ name: "PrivacyPolicy", setup() { const {isLoading, isError, data, error} = useGetAdditionalContent(PageType.PRIVACY_POLICY); + + const safeDownloadUrl = computed(() => { + if (!data.value?.textItemView[0]?.link) return null; + return isValidDownloadUrl(data.value.textItemView[0].link) + ? data.value.textItemView[0].link + : null; + }); return { isLoading, isError, - item: data, + item: safeDownloadUrl, error }; } }); </script>Committable suggestion skipped: line range outside the PR's diff.
frontend/src/features/admin/features/the-offers/components/NewOfferListItem.vue (1)
31-41: 🛠️ Refactor suggestion
Enhance prop validation and type safety
The props lack required validation and specific type definitions. Consider adding proper validation and TypeScript types.
props: { label: { type: Object as () => I18nLabel, + required: true, + validator: (value: I18nLabel) => { + return typeof value.addNewTitle === 'string' + && typeof value.addNewDescription === 'string'; + } }, setIsAddNew: { type: Function, + required: true, + validator: (value: Function) => typeof value === 'function' }, disabled: { type: Boolean, default: false } -} +}, +emits: ['click']Committable suggestion skipped: line range outside the PR's diff.
frontend/src/features/commons/middleware/useGetOffers.ts (2)
19-25: 🛠️ Refactor suggestion
Add documentation and defensive checks
This function contains important business logic and should be documented. Also, consider adding defensive checks for null/undefined values.
+/** + * Filters offers based on user's admin status and department. + * @param listItems - List of offers to filter + * @param adminUserInfo - Admin user information containing permissions + * @returns Filtered list of offers that the user can edit + */ const filterForEditableOffers = (listItems: OfferListItem[], adminUserInfo: AdminUserInfo) => { + if (!listItems?.length || !adminUserInfo) { + return []; + } if (adminUserInfo.isCentralAdmin) { return listItems } else { - return listItems.filter(it => it.departments.includes(adminUserInfo.department)); + return listItems.filter(it => it.departments?.includes(adminUserInfo.department)); } }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements./** * Filters offers based on user's admin status and department. * @param listItems - List of offers to filter * @param adminUserInfo - Admin user information containing permissions * @returns Filtered list of offers that the user can edit */ const filterForEditableOffers = (listItems: OfferListItem[], adminUserInfo: AdminUserInfo) => { if (!listItems?.length || !adminUserInfo) { return []; } if (adminUserInfo.isCentralAdmin) { return listItems } else { return listItems.filter(it => it.departments?.includes(adminUserInfo.department)); } }
27-36: 🛠️ Refactor suggestion
Refactor promise chain and improve error handling
The current implementation could be improved by:
- Using async/await for better readability
- Adding proper error handling
- Making the query key more specific
export const useGetEditableOffers = () => useQuery<OfferListItem[]>( - ['adminListItems'], + ['offers', 'editable'], - () => getOffers() - .then((listItems) => { - return getAdminUserInfo().then(adminInfo => { - return filterForEditableOffers(listItems, adminInfo) - }) - }) + async () => { + try { + const [listItems, adminInfo] = await Promise.all([ + getOffers(), + getAdminUserInfo() + ]); + return filterForEditableOffers(listItems, adminInfo); + } catch (error) { + console.error('Failed to fetch editable offers:', error); + throw error; + } + } )📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.export const useGetEditableOffers = () => useQuery<OfferListItem[]>( ['offers', 'editable'], async () => { try { const [listItems, adminInfo] = await Promise.all([ getOffers(), getAdminUserInfo() ]); return filterForEditableOffers(listItems, adminInfo); } catch (error) { console.error('Failed to fetch editable offers:', error); throw error; } } )
frontend/src/features/admin/features/the-offers/middelware/useOffers.ts (3)
23-30:
⚠️ Potential issueFix parameter naming and enhance deletion safety.
The parameter name
contactPointToDelete
suggests this code was copied from another context. Also, consider adding validation before deletion.export const useDeleteOffer = () => useMutation({ - mutationFn: async (contactPointToDelete: Offer) => { - if (contactPointToDelete.id) { - return deleteOffer(contactPointToDelete); + mutationFn: async (offerToDelete: Offer) => { + if (!offerToDelete.id) { + throw new Error('Cannot delete offer: ID is required'); } - throw new Error('ID is missing'); + // Validate that the offer exists before deletion + return deleteOffer(offerToDelete); } });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.export const useDeleteOffer = () => useMutation({ mutationFn: async (offerToDelete: Offer) => { if (!offerToDelete.id) { throw new Error('Cannot delete offer: ID is required'); } // Validate that the offer exists before deletion return deleteOffer(offerToDelete); } });
10-20: 🛠️ Refactor suggestion
Consider enhancing error handling and type safety.
While the basic functionality is correct, there are several improvements that could make this hook more robust:
export const useUpdateOffer = () => useMutation({ mutationFn: async (useOffers: UseOffers) => { if (useOffers.id) { - const headers = { + const headers: Record<string, string> = { "Content-Type": "multipart/form-data", }; + if (useOffers.file) { + const maxSize = 5 * 1024 * 1024; // 5MB + if (useOffers.file.size > maxSize) { + throw new Error('File size exceeds 5MB limit'); + } + if (!useOffers.file.type.startsWith('image/')) { + throw new Error('Only image files are allowed'); + } + } return await putOffer(useOffers.id, useOffers.offer, useOffers.file, headers); } - throw new Error('ID is missing'); + throw new Error('Offer ID is required for updates'); } });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.export const useUpdateOffer = () => useMutation({ mutationFn: async (useOffers: UseOffers) => { if (useOffers.id) { const headers: Record<string, string> = { "Content-Type": "multipart/form-data", }; if (useOffers.file) { const maxSize = 5 * 1024 * 1024; // 5MB if (useOffers.file.size > maxSize) { throw new Error('File size exceeds 5MB limit'); } if (!useOffers.file.type.startsWith('image/')) { throw new Error('Only image files are allowed'); } } return await putOffer(useOffers.id, useOffers.offer, useOffers.file, headers); } throw new Error('Offer ID is required for updates'); } });
32-41: 🛠️ Refactor suggestion
Add type safety and consistent error handling.
The creation hook needs better type safety and should maintain consistency with other hooks' error handling patterns.
export const useCreateNewOffer = () => useMutation({ - mutationFn: async (newOffers: UseOffers) => { + mutationFn: async (newOffers: UseOffers): Promise<void> => { + if (!newOffers.offer.title || !newOffers.offer.description) { + throw new Error('Title and description are required'); + } const headers = { "Content-Type": "multipart/form-data", }; + if (newOffers.file) { + const maxSize = 5 * 1024 * 1024; // 5MB + if (newOffers.file.size > maxSize) { + throw new Error('File size exceeds 5MB limit'); + } + if (!newOffers.file.type.startsWith('image/')) { + throw new Error('Only image files are allowed'); + } + } await postOffer(newOffers.offer, newOffers.file, headers); }, });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.export const useCreateNewOffer = () => useMutation({ mutationFn: async (newOffers: UseOffers): Promise<void> => { if (!newOffers.offer.title || !newOffers.offer.description) { throw new Error('Title and description are required'); } const headers = { "Content-Type": "multipart/form-data", }; if (newOffers.file) { const maxSize = 5 * 1024 * 1024; // 5MB if (newOffers.file.size > maxSize) { throw new Error('File size exceeds 5MB limit'); } if (!newOffers.file.type.startsWith('image/')) { throw new Error('Only image files are allowed'); } } await postOffer(newOffers.offer, newOffers.file, headers); }, });
frontend/src/features/admin/features/the-offers/api/OffersManipulationClient.ts (3)
56-62:
⚠️ Potential issueImprove error handling and null checks
The function has several issues:
- Redundant null check
|| offer.imageLink != undefined
- Missing error handling for S3 operations
- Risk of orphaned S3 files if offer deletion fails
Consider this improved implementation:
export const deleteOffer = async (offer: Offer) => { - if(offer.imageLink != null || offer.imageLink != undefined){ - const deleteOldFile = `/s3/delete?link=${encodeURIComponent(offer.imageLink.toString())}`; - await httpDeleteS3File(deleteOldFile); - } - return httpDeleteJson("/offers/" + offer.id); + try { + if (offer.imageLink) { + const deleteOldFile = `/s3/delete?link=${encodeURIComponent(offer.imageLink)}`; + await httpDeleteS3File(deleteOldFile); + } + return await httpDeleteJson("/offers/" + offer.id); + } catch (error) { + // If offer deletion fails after successful image deletion, + // log the orphaned image for cleanup + console.error(`Failed to delete offer: ${error.message}`); + if (error.message.includes('image deleted but offer deletion failed')) { + console.error(`Orphaned S3 image: ${offer.imageLink}`); + } + throw error; + } }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.export const deleteOffer = async (offer: Offer) => { try { if (offer.imageLink) { const deleteOldFile = `/s3/delete?link=${encodeURIComponent(offer.imageLink)}`; await httpDeleteS3File(deleteOldFile); } return await httpDeleteJson("/offers/" + offer.id); } catch (error) { // If offer deletion fails after successful image deletion, // log the orphaned image for cleanup console.error(`Failed to delete offer: ${error.message}`); if (error.message.includes('image deleted but offer deletion failed')) { console.error(`Orphaned S3 image: ${offer.imageLink}`); } throw error; } }
5-25:
⚠️ Potential issueAdd error handling and validation for file upload
The function lacks several important validations and error handling:
- File size and type validation before upload
- Error handling for S3 upload failures
- Type validation for the imageLink response
Consider implementing these improvements:
export const postOffer = async (offer: Offer, file?: File, headers?: Record<string, string>) => { + const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB + const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif']; if (file) { + if (file.size > MAX_FILE_SIZE) { + throw new Error('File size exceeds 5MB limit'); + } + if (!ALLOWED_TYPES.includes(file.type)) { + throw new Error('Invalid file type. Allowed types: JPG, PNG, GIF'); + } const formData = new FormData(); formData.append("file", file); - const linkResponse = await httpPostMultipart<string>( - `/s3/upload`, - formData, - headers - ); + try { + const linkResponse = await httpPostMultipart<string>( + `/s3/upload`, + formData, + headers + ); + if (!linkResponse || typeof linkResponse !== 'string') { + throw new Error('Invalid response from S3 upload'); + } + offer.imageLink = linkResponse; + } catch (error) { + throw new Error(`Failed to upload image: ${error.message}`); + } - offer.imageLink = linkResponse; } return httpPostJson<Offer>("/offers", offer); };📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.export const postOffer = async (offer: Offer, file?: File, headers?: Record<string, string>) => { const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif']; if (file) { if (file.size > MAX_FILE_SIZE) { throw new Error('File size exceeds 5MB limit'); } if (!ALLOWED_TYPES.includes(file.type)) { throw new Error('Invalid file type. Allowed types: JPG, PNG, GIF'); } const formData = new FormData(); formData.append("file", file); try { const linkResponse = await httpPostMultipart<string>( `/s3/upload`, formData, headers ); if (!linkResponse || typeof linkResponse !== 'string') { throw new Error('Invalid response from S3 upload'); } offer.imageLink = linkResponse; } catch (error) { throw new Error(`Failed to upload image: ${error.message}`); } } return httpPostJson<Offer>("/offers", offer); };
28-54: 🛠️ Refactor suggestion
Refactor for better error handling and code reuse
The function has several areas for improvement:
- Duplicate file validation logic with postOffer
- Inadequate error handling for S3 operations
- Inconsistent null checks
Consider extracting shared logic and improving error handling:
+const validateAndUploadFile = async (file: File, headers?: Record<string, string>) => { + const MAX_FILE_SIZE = 5 * 1024 * 1024; + const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif']; + + if (file.size > MAX_FILE_SIZE) { + throw new Error('File size exceeds 5MB limit'); + } + if (!ALLOWED_TYPES.includes(file.type)) { + throw new Error('Invalid file type. Allowed types: JPG, PNG, GIF'); + } + + const formData = new FormData(); + formData.append("file", file); + return httpPostMultipart<string>(`/s3/upload`, formData, headers); +}; export const putOffer = async (id: string, offer: Offer, file?: File, headers?: Record<string, string>) => { if (file) { - const formData = new FormData(); - formData.append("file", file); - - if(offer.imageLink != null){ - formData.append("link", offer.imageLink.toString()) - - const deleteOldFile = `/s3/delete?link=${encodeURIComponent(offer.imageLink.toString())}`; - await httpDeleteS3File(deleteOldFile); - } - - const newFileLink = await httpPostMultipart<string>( - `/s3/upload`, - formData, - headers - ); - - offer.imageLink = newFileLink; + try { + if (offer.imageLink) { + const deleteOldFile = `/s3/delete?link=${encodeURIComponent(offer.imageLink)}`; + await httpDeleteS3File(deleteOldFile); + } + + const newFileLink = await validateAndUploadFile(file, headers); + if (!newFileLink || typeof newFileLink !== 'string') { + throw new Error('Invalid response from S3 upload'); + } + offer.imageLink = newFileLink; + } catch (error) { + throw new Error(`Failed to update image: ${error.message}`); + } } return httpPutJson<Offer>("/offers/" + id, offer); }Committable suggestion skipped: line range outside the PR's diff.
frontend/src/features/admin/features/the-offers/i18n.ts (1)
28-31:
⚠️ Potential issueRemove duplicate Map entries.
The following entries are duplicated and will cause the second entry to override the first:
ADD_LINK
on lines 28 and 29ADD_FILE
on lines 30 and 31Apply this diff to remove the duplicates:
[AdminOffersLabels.ADD_CONTACT, `${i18n.t(`${ADMIN_OFFERS_PATH}.${AdminOffersLabels.ADD_CONTACT}`)}`], [AdminOffersLabels.ADD_LINK, `${i18n.t(`${ADMIN_OFFERS_PATH}.${AdminOffersLabels.ADD_LINK}`)}`], - [AdminOffersLabels.ADD_LINK, `${i18n.t(`${ADMIN_OFFERS_PATH}.${AdminOffersLabels.ADD_LINK}`)}`], [AdminOffersLabels.ADD_FILE, `${i18n.t(`${ADMIN_OFFERS_PATH}.${AdminOffersLabels.ADD_FILE}`)}`], - [AdminOffersLabels.ADD_FILE, `${i18n.t(`${ADMIN_OFFERS_PATH}.${AdminOffersLabels.ADD_FILE}`)}`],📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.[AdminOffersLabels.ADD_LINK, `${i18n.t(`${ADMIN_OFFERS_PATH}.${AdminOffersLabels.ADD_LINK}`)}`], [AdminOffersLabels.ADD_FILE, `${i18n.t(`${ADMIN_OFFERS_PATH}.${AdminOffersLabels.ADD_FILE}`)}`],
frontend/src/features/the-footer-main/the-footer-main.vue (2)
2-8:
⚠️ Potential issueFix incorrect Vuetify class name
The class
justify-right
is not a standard Vuetify class. Usejustify-end
instead to align content to the right.- class="align-center justify-right" + class="align-center justify-end"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.<v-container> <v-footer min-height="75px" padless color="transparent" class="align-center justify-end" >
20-28:
⚠️ Potential issueRemove unused tooltip directives
The button has
v-bind="attrs"
andv-on="on"
directives but nov-tooltip
wrapper. These directives won't have any effect without the tooltip context.<v-btn class="mx-2 pt-0 mt-0" color="secondary" - v-bind="attrs" @click="openInfoDialog" - v-on="on" >📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.<v-btn class="mx-2 pt-0 mt-0" color="secondary" @click="openInfoDialog" > <v-icon>mdi-information-outline</v-icon> {{ infoLabels.informationTitle }} </v-btn>
frontend/src/features/commons/base-link-card/base-link-card.vue (1)
99-104: 🛠️ Refactor suggestion
Consider refactoring the CSS class implementation.
The current implementation has several potential issues:
- Using
!important
flags should be avoided as they make styles harder to maintain- The class name "title" might conflict with Vuetify's built-in classes
- The
text-indent: 0em
property with!important
suggests fighting against inherited stylesConsider this alternative implementation:
-.title{ - white-space: pre-wrap !important; - text-indent: 0em !important; - word-break: keep-all; - line-height: normal; -} +.base-link-card-title { + white-space: pre-wrap; + word-break: keep-all; + line-height: normal; +}Then update the template:
-<v-card-title class="title"> +<v-card-title class="base-link-card-title">This approach:
- Uses a more specific class name to avoid conflicts
- Removes unnecessary
!important
flags- Removes redundant
text-indent
propertyCommittable suggestion skipped: line range outside the PR's diff.
frontend/src/features/admin/features/the-offers/components/SaveNewButton.vue (3)
74-82: 🛠️ Refactor suggestion
Enhance error handling
The error handling could be improved:
- Reset loading state on error
- Add type safety for error object
- Consider using a dedicated error handling utility
-catch((error) => { +catch((error: unknown) => { + isLoading.value = false; const fallbackErrorMessage = "An unexpected error occurred"; - const customErrorMessage = error.response?.data?.message || fallbackErrorMessage; + const customErrorMessage = error instanceof Error + ? error.message + : fallbackErrorMessage; errorMessage.value = customErrorMessage; isWriteError.value = true; emit("error", customErrorMessage); });Committable suggestion skipped: line range outside the PR's diff.
34-44: 🛠️ Refactor suggestion
Enhance prop definitions with validation
The props could benefit from additional validation and type safety:
offerToSave
should be marked as requiredfile
type should be more specific withFile | null
- Add validation for
offerToSave
propertiesofferToSave: { type: Object as () => Offer, + required: true, + validator: (value: Offer) => { + return value && typeof value.title === 'string' + } }, disabled: { type: Boolean, default: false }, file: { - type: File, + type: [File, null] as PropType<File | null>, default: null }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.offerToSave: { type: Object as () => Offer, required: true, validator: (value: Offer) => { return value && typeof value.title === 'string' } }, disabled: { type: Boolean, default: false }, file: { type: [File, null] as PropType<File | null>, default: null }
68-71:
⚠️ Potential issueImprove navigation handling
The current implementation has several issues:
- Using
router.go(0)
forces a page reload which is an anti-pattern in SPAs- Hardcoded route path reduces maintainability
- Potential race condition with setTimeout if multiple saves are attempted
-setTimeout(() => { - router.push("/admin/angebote/"); - router.go(0); -}, 1000); // delay for 1 second +const ROUTES = { + OFFERS_LIST: '/admin/angebote/' +}; +await router.push(ROUTES.OFFERS_LIST);Committable suggestion skipped: line range outside the PR's diff.
frontend/src/features/the-offers/the-offer.vue (6)
27-27:
⚠️ Potential issueSecurity Warning: Unsafe HTML rendering
Using
v-html
to render user-provided content can expose the application to XSS attacks. Consider using a sanitization library like DOMPurify.-<span v-html="offer.description" /> +<span v-html="sanitizeHtml(offer.description)" />Add the following import and sanitization:
import DOMPurify from 'dompurify'; // In setup(): const sanitizeHtml = (html: string) => DOMPurify.sanitize(html);
72-72:
⚠️ Potential issueHandle error state in the UI
The error state from
useGetOffer
is destructured but never used in the template. Consider displaying error messages to users.-const { isLoading, data: offer, error } = useGetOffer(offerId); +const { isLoading, data: offer, error } = useGetOffer(offerId); + +const showError = computed(() => { + if (error.value) { + return `Failed to load offer: ${error.value.message}`; + } + return null; +});And in template:
<v-alert v-if="showError" type="error" class="mb-4" > {{ showError }} </v-alert>
71-72: 🛠️ Refactor suggestion
Add type safety for offer data
Define an interface for the offer structure to ensure type safety throughout the component.
interface Offer { id: string; title: string; description: string; startDate?: string; endDate?: string; imageLink: string; } // Then use it: const offer = ref<Offer | null>(null);
39-44:
⚠️ Potential issueAdd alt text for accessibility
Images should have descriptive alt text for screen readers and fallback scenarios.
<img :src="offer.imageLink" + :alt="offer.title || 'Offer image'" class="cropped-image" contain >
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.<img :src="offer.imageLink" :alt="offer.title || 'Offer image'" class="cropped-image" contain > </div>
74-76: 🛠️ Refactor suggestion
Avoid hard-coded routes
Using hard-coded route strings can lead to maintenance issues. Consider using route names or constants.
-const back = () => { - router.push("/angebot"); -}; +const ROUTES = { + OFFERS_LIST: '/angebot' +} as const; + +const back = () => { + router.push(ROUTES.OFFERS_LIST); +};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.const ROUTES = { OFFERS_LIST: '/angebot' } as const; const back = () => { router.push(ROUTES.OFFERS_LIST); };
90-92: 🛠️ Refactor suggestion
Add missing image styles
The template uses
image-container
,image-wrapper
, andcropped-image
classes, but they're not defined in the style section.<style scoped> - +.image-container { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.image-wrapper { + width: 100%; + max-width: 500px; + aspect-ratio: 16/9; + overflow: hidden; +} + +.cropped-image { + width: 100%; + height: 100%; + object-fit: cover; +} </style>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.<style scoped> .image-container { width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; } .image-wrapper { width: 100%; max-width: 500px; aspect-ratio: 16/9; overflow: hidden; } .cropped-image { width: 100%; height: 100%; object-fit: cover; } </style>
frontend/src/features/admin/features/the-offers/components/DeleteButton.vue (2)
83-97: 🛠️ Refactor suggestion
Improve error handling and navigation flow
The current implementation has several potential issues:
- The delete event is emitted before the actual deletion occurs
- Using
router.go(0)
forces a full page refresh, which is not ideal for SPA- Error handling doesn't provide user feedback
Consider this improved implementation:
const deleteOffer = () => { if (props.currentItem) { - emit("delete", props.currentItem); - } - isDialogActive.value = false; - mutateAsync(props.currentItem) - .then(() => { - isSnackbarActive.value = true; - setTimeout(() => { - router.push("/admin/angebote/"); - router.go(0); - }, 1000); // delay for 1 second - }) - .catch(() => emit("error")); + isDialogActive.value = false; + mutateAsync(props.currentItem) + .then(() => { + emit("delete", props.currentItem); + isSnackbarActive.value = true; + setTimeout(() => { + router.push("/admin/angebote/"); + }, REDIRECT_DELAY); + }) + .catch((error) => { + emit("error", error); + isSnackbarActive.value = true; + snackbarMessage.value = "Fehler beim Löschen des Angebots"; + }); + } };Committable suggestion skipped: line range outside the PR's diff.
21-24:
⚠️ Potential issueFix grammatical errors in the German text
The dialog text contains grammatical errors:
- "das Angebote" should be "das Angebot" (singular)
- "der Angebote" should be "des Angebots" (singular)
- <v-card>Soll das Angebote wirklich gelöscht werden?</v-card> + <v-card>Soll das Angebot wirklich gelöscht werden?</v-card> <v-card-text> - Das Löschen der Angebote kann nicht rückgängig gemacht werden. Eine Löschung kann nur durch das - erneute Anlegen der Angebote bereinigt werden. + Das Löschen des Angebots kann nicht rückgängig gemacht werden. Eine Löschung kann nur durch das + erneute Anlegen des Angebots bereinigt werden.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.<v-card>Soll das Angebot wirklich gelöscht werden?</v-card> <v-card-text> Das Löschen des Angebots kann nicht rückgängig gemacht werden. Eine Löschung kann nur durch das erneute Anlegen des Angebots bereinigt werden.
frontend/src/features/admin/features/the-offers/components/SaveUpdateButton.vue (3)
81-84:
⚠️ Potential issueRemove anti-pattern page reload and handle navigation properly.
Using
router.go(0)
forces a page reload which is an anti-pattern in Vue applications. This can cause unnecessary server requests and a poor user experience.- setTimeout(() => { - router.push("/admin/angebote/" + editedOffer.value?.id); - router.go(0); - }, 1000); // delay for 1 second + await router.push("/admin/angebote/" + editedOffer.value?.id);Committable suggestion skipped: line range outside the PR's diff.
33-48: 🛠️ Refactor suggestion
Strengthen props validation and documentation.
The props need stricter validation and documentation for better maintainability and type safety.
props: { offerToSave: { type: Object as () => Offer, + required: true, + validator: (value: Offer) => { + return value !== null && typeof value === 'object'; + } }, id: { type: String, + required: true }, disabled: { type: Boolean, default: false, + validator: (value: boolean) => typeof value === 'boolean' }, file: { type: File, default: null, + validator: (value: File | null) => value === null || value instanceof File } }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.props: { offerToSave: { type: Object as () => Offer, required: true, validator: (value: Offer) => { return value !== null && typeof value === 'object'; } }, id: { type: String, required: true }, disabled: { type: Boolean, default: false, validator: (value: boolean) => typeof value === 'boolean' }, file: { type: File, default: null, validator: (value: File | null) => value === null || value instanceof File } },
61-100: 🛠️ Refactor suggestion
Refactor saveAdd function for better error handling and cleanup.
The function has several issues including potential memory leaks and unnecessary object copying.
+ const timeoutRef = ref<number | null>(null); + + onBeforeUnmount(() => { + if (timeoutRef.value) { + clearTimeout(timeoutRef.value); + } + }); function saveAdd(file?: File | null) { - const headers = { - 'Content-Type': 'multipart/form-data' - }; if (!props.offerToSave?.imageLink || props.offerToSave.imageLink.length === 0) { emit("error", "Bild ist erforderlich."); return; } - editedOffer.value = { ...props.offerToSave }; // Create a copy of offerToSave - if (editedOffer.value) { + if (props.offerToSave) { mutateAsync({ - offer: editedOffer.value, + offer: props.offerToSave, id: props.offerToSave.id, file: file ? file : undefined, - image: editedOffer.value.imageLink, + image: props.offerToSave.imageLink, }) .then(() => { showSuccessSnackbar(); - setTimeout(() => { - router.push("/admin/angebote/" + editedOffer.value?.id); - router.go(0); - }, 1000); + timeoutRef.value = window.setTimeout(async () => { + await router.push("/admin/angebote/" + props.offerToSave.id); + }, 1000); }) .catch((error) => { const fallbackErrorMessage = "An unexpected error occurred"; const customErrorMessage = error.response?.data?.message || fallbackErrorMessage; errorMessage.value = customErrorMessage; isWriteError.value = true; - - // Emit the error event to the parent component emit("error", customErrorMessage); }); } else { - // handle case where editedOffer.value is null emit("error", "No offer is available for editing."); } }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.const timeoutRef = ref<number | null>(null); onBeforeUnmount(() => { if (timeoutRef.value) { clearTimeout(timeoutRef.value); } }); function saveAdd(file?: File | null) { if (!props.offerToSave?.imageLink || props.offerToSave.imageLink.length === 0) { emit("error", "Bild ist erforderlich."); return; } if (props.offerToSave) { mutateAsync({ offer: props.offerToSave, id: props.offerToSave.id, file: file ? file : undefined, image: props.offerToSave.imageLink, }) .then(() => { showSuccessSnackbar(); timeoutRef.value = window.setTimeout(async () => { await router.push("/admin/angebote/" + props.offerToSave.id); }, 1000); }) .catch((error) => { const fallbackErrorMessage = "An unexpected error occurred"; const customErrorMessage = error.response?.data?.message || fallbackErrorMessage; errorMessage.value = customErrorMessage; isWriteError.value = true; emit("error", customErrorMessage); }); } else { emit("error", "No offer is available for editing."); } }
frontend/src/features/admin/features/the-additional/features/the-faq/the-faq.vue (1)
54-57:
⚠️ Potential issueRemove duplicate BackButton component.
The BackButton component appears twice in the template (lines 3 and 54-57) with the same callback function. This violates the DRY principle and might confuse users. Consider keeping only one instance, preferably at the top of the page.
- <BackButton - :callback="back" - class="mt-1" - />Committable suggestion skipped: line range outside the PR's diff.
frontend/src/features/admin/features/the-additional/features/the-downloads/the-downloads.vue (2)
54-57:
⚠️ Potential issueRemove duplicate BackButton
There are two identical BackButtons with the same callback. This is redundant and could be confusing for users. Consider keeping only one, preferably at the top of the page for consistency with other admin pages.
-<BackButton - :callback="back" - class="mt-1" ->
47-47:
⚠️ Potential issueFix typo in prop name
filteredDownloadsy
The prop name appears to have an unintended 'y' at the end. This should be renamed to
filteredDownloads
for clarity and consistency.-:items="filteredDownloadsy" +:items="filteredDownloads"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.:items="filteredDownloads"
frontend/src/features/admin/features/the-additional/features/the-glossar/the-glossar.vue (2)
85-88:
⚠️ Potential issueRemove duplicate BackButton component
There are two instances of the BackButton component in the template (one at the top and one at the bottom) with the same callback. This duplication could be confusing for users and maintenance.
Consider keeping only one instance, preferably at the top of the page following common navigation patterns.
51-71: 🛠️ Refactor suggestion
Enhance accessibility and simplify text alignment
A few suggestions to improve this section:
- The text alignment classes can be simplified
- Buttons should have type attributes for better semantics
- Consider using a more semantic separator
Consider applying these improvements:
- <p class="text-xs-center text-sm-center text-md-center text-lg-center text-xl-center"> + <p class="text-center"> <button + type="button" :class="{ active: filterLetter === '' }" class="pa-2" @click="filterLetter = ''" > Filter Zurücksetzen </button> <span v-for="(letter, letterIndex) in glossaryAlphabet" :key="letterIndex" ><button + type="button" :key="letterIndex" :class="{ active: filterLetter === letter }" class="pa-2" @click="filterLetter = letter" >{{ letter - }}</button><span v-if="letterIndex + 1 !== glossaryAlphabet.length">·</span></span> + }}</button><span v-if="letterIndex + 1 !== glossaryAlphabet.length" aria-hidden="true" class="mx-1">·</span></span>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.<p class="text-center"> <button type="button" :class="{ active: filterLetter === '' }" class="pa-2" @click="filterLetter = ''" > Filter Zurücksetzen </button> <span v-for="(letter, letterIndex) in glossaryAlphabet" :key="letterIndex" ><button type="button" :key="letterIndex" :class="{ active: filterLetter === letter }" class="pa-2" @click="filterLetter = letter" >{{ letter }}</button><span v-if="letterIndex + 1 !== glossaryAlphabet.length" aria-hidden="true" class="mx-1">·</span></span> </p>
frontend/src/core/translations/de-DE.json (3)
87-87:
⚠️ Potential issueFix grammatical error in content items description
The article doesn't match the gender of "Artikel" (masculine).
Apply this correction:
- "addNewDescription": "Fügen Sie eine neuen Artikel hinzu.", + "addNewDescription": "Fügen Sie einen neuen Artikel hinzu.",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements."addNewDescription": "Fügen Sie einen neuen Artikel hinzu.",
69-83: 💡 Codebase verification
Fix grammatical inconsistencies in German translations
Several grammatical issues found in the translations:
"addNewDescription"
in offers section: "Fügen Sie eine neues Angebot hinzu" has incorrect article agreement (should be "ein neues")"edit"
in offers section refers to "Anlaufstelle" but is used in the offers context"addNewDescription"
in contentItems: "Fügen Sie eine neuen Artikel" has incorrect article agreement (should be "einen neuen")The translations should be:
- offers.texts.addNewDescription: "Fügen Sie ein neues Angebot hinzu."
- offers.texts.edit: "Bearbeiten Sie ein bestehendes Angebot."
- contentItems.texts.addNewDescription: "Fügen Sie einen neuen Artikel hinzu."
🔗 Analysis chain
Verify translation consistency across similar sections
The file has multiple sections (contactPoints, offers, contentItems) with similar translation keys but different contexts. Let's verify the consistency of translations across these sections.
Also applies to: 87-87
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Compare translation keys and their values across similar sections # Extract and compare the structure of similar sections jq -r '.Admin | {contactPoints,offers,contentItems}' frontend/src/core/translations/de-DE.json # List all occurrences of potentially inconsistent terms echo "Checking for 'Anlaufstelle'..." rg -i "anlaufstelle" frontend/src/core/translations/ echo "Checking for article agreement..." rg -i "fügen sie eine" frontend/src/core/translations/Length of output: 4348
69-83:
⚠️ Potential issueFix grammatical errors and incorrect terminology in offers section
There are several issues in the German translations:
- Grammar: "eine neues" should be "ein neues" (line 72)
- Context: "Anlaufstelle" should be "Angebot" in the edit message (line 77)
Apply these corrections:
"offers": { "texts": { "addNewTitle": "Hinzufügen", - "addNewDescription": "Fügen Sie eine neues Angebot hinzu.", + "addNewDescription": "Fügen Sie ein neues Angebot hinzu.", "addDepartment": "Referat", "addContact": "Kontakt hinzufügen", "addLink": "Link hinzufügen", "addFile": "Datei hinzufügen", - "edit": "Bearbeiten Sie eine bestehende Anlaufstelle.", + "edit": "Bearbeiten Sie ein bestehendes Angebot.", "cancel": "Abbrechen", "save": "Speichern",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements."offers": { "texts": { "addNewTitle": "Hinzufügen", "addNewDescription": "Fügen Sie ein neues Angebot hinzu.", "addDepartment": "Referat", "addContact": "Kontakt hinzufügen", "addLink": "Link hinzufügen", "addFile": "Datei hinzufügen", "edit": "Bearbeiten Sie ein bestehendes Angebot.", "cancel": "Abbrechen", "save": "Speichern", "markdownHint": "Die Beschreibung kann mittels Markdown (Empfehlung) oder HTML designed werden. Das Ergebnis sehen Sie in dem Feld daneben. Über diesen Link können Sie sich die verschiedenen Möglichkeiten von Markdown ansehen: ", "addDepartmentHint":"Um ein weiteres Referat hinzuzufügen geben Sie dies ein und drücken Sie bitte <kbd>enter</kbd>." } },
frontend/src/features/admin/features/the-contact-points/the-contact-points-overview.vue (2)
26-52: 🛠️ Refactor suggestion
Simplify list structure and enhance accessibility
The current implementation has some structural and accessibility concerns:
- There's unnecessary nesting of
v-list
components which adds complexity- The list structure could benefit from proper ARIA attributes for better accessibility
Consider applying these changes:
- <v-list> <NewContactPointListItem :label="label" :set-is-add-new="setIsAddNew" :disabled="selectedItem !== undefined || isAddNew" /> - </v-list> <v-list v-if="listItems && listItems.length > 0"> <v-list-item v-for="item in listItems" :key="item.id" three-line link :disabled="selectedItem !== undefined || isAddNew" + :aria-selected="item === selectedItem" + role="option" :class="{ 'selected': item === selectedItem }" @click="setSelectedItem(item)" >Committable suggestion skipped: line range outside the PR's diff.
107-108: 💡 Codebase verification
Remove unused import
useGetAdminUserInfo
The
useGetAdminUserInfo
hook is imported but not used anywhere in the component. It should be removed to keep the codebase clean.🔗 Analysis chain
Verify usage of imported admin user info hook
The
useGetAdminUserInfo
hook is imported but doesn't appear to be used in the component.Let's verify if this import is needed:
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Check if useGetAdminUserInfo is used in the component ast-grep --pattern 'useGetAdminUserInfo' frontend/src/features/admin/features/the-contact-points/the-contact-points-overview.vue # Check if it's used in related components rg -l 'useGetAdminUserInfo' frontend/src/features/admin/Length of output: 678
Script:
#!/bin/bash # Check the actual usage of useGetAdminUserInfo in the component ast-grep --pattern 'useGetAdminUserInfo($$$)' frontend/src/features/admin/features/the-contact-points/the-contact-points-overview.vue # Get more context around any potential usage rg -C 3 'useGetAdminUserInfo' frontend/src/features/admin/features/the-contact-points/the-contact-points-overview.vueLength of output: 774
frontend/src/features/the-offers/the-offers.vue (5)
154-154:
⚠️ Potential issueHandle error state in the template.
The component retrieves error state from
useGetOfferListItems
but doesn't display error messages to users.-<div v-else> +<div v-else-if="isError"> + {{ error?.message || 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.' }} +</div> +<div v-else> Zurzeit sind keine Angebote verfügbar. Schauen Sie später wieder vorbei. </div>Also applies to: 123-125
109-109:
⚠️ Potential issueFix invalid style attribute syntax.
The style attribute has invalid syntax:
style="font-size:14;"
. The value is missing a unit.-<v-card-title style="font-size:14;"> +<v-card-title style="font-size: 14px;">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.<v-card-title style="font-size: 14px;">
45-45:
⚠️ Potential issueFix invalid style syntax.
The inline style has incorrect syntax:
style="height:275px; !important"
. The semicolon before!important
is invalid.-style="height:275px; !important" +style="height: 275px !important"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.style="height: 275px !important"
49-54:
⚠️ Potential issueAdd alt text for accessibility.
Images are missing alt text attributes, which is essential for accessibility. Add descriptive alt text for screen readers.
<img :src="item.imageLink" + :alt="item.title" class="cropped-image" contain >
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.<img :src="item.imageLink" :alt="item.title" class="cropped-image" contain > </div>
211-221:
⚠️ Potential issueFix transform translate percentages.
The transform translate percentages don't match the top/left positioning values.
.image-fill { position: absolute; top: 25%; left: 25%; - transform: translate(-25%, -25%); + transform: translate(-50%, -50%); width: 100%; height: 100%; background-size: cover; background-repeat: no-repeat; background-position: center; }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements..image-fill { position: absolute; top: 25%; left: 25%; transform: translate(-50%, -50%); width: 100%; height: 100%; background-size: cover; background-repeat: no-repeat; background-position: center; }
frontend/src/features/admin/features/the-additional/features/the-leadership-cooperation/the-leadership-cooperation.vue (2)
157-160: 🛠️ Refactor suggestion
Remove duplicate BackButton component
There are two instances of
BackButton
with the same callback. The second instance (at the bottom) appears to be redundant as it serves the same purpose as the one at the top of the page.- <BackButton - :callback="back" - class="mt-1" - />
126-131:
⚠️ Potential issueSecurity: Potential XSS vulnerability in markdown rendering
Using
v-html
with user-provided content could expose the application to XSS attacks. Consider using a secure markdown renderer with proper sanitization.Consider using DOMPurify:
+ import DOMPurify from 'dompurify'; // In computed property: - v-html="computeMarkdown" + v-html="DOMPurify.sanitize(computeMarkdown)"Committable suggestion skipped: line range outside the PR's diff.
frontend/src/features/admin/features/the-additional/features/the-conflict-prevention/the-conflict-prevention.vue (3)
9-9: 🛠️ Refactor suggestion
Remove duplicate BackButton component
The BackButton component appears twice in the template with the same callback. This violates the DRY principle and could be confusing for users.
Keep only one instance of the BackButton, preferably at the top of the page (line 9).
Also applies to: 157-160
31-96: 🛠️ Refactor suggestion
⚠️ Potential issueRefactor text editor implementation for better security and maintainability
Several concerns with the current implementation:
- Direct DOM manipulation via
querySelector
is anti-pattern in Vue- Using HTML tags for formatting could lead to XSS vulnerabilities
- The formatting logic could be more maintainable
Consider these improvements:
- Use Vue refs instead of querySelector
- Use markdown syntax instead of HTML tags
- Extract formatting logic into a separate composable
Example refactor:
- const textarea = document.querySelector('#description-textarea') as HTMLTextAreaElement | null; + const textareaRef = ref<HTMLTextAreaElement | null>(null);<v-textarea ref="textareaRef" ... />
Also, consider using a dedicated markdown editor component like
vue-markdown-editor
which handles these concerns securely.Also applies to: 226-270
126-130: 💡 Codebase verification
⚠️ Potential issueMarkdown content requires XSS protection
The verification confirms that the application uses raw markdown parsing without any sanitization:
computeMarkdown
directly usesmarked.parse()
without sanitization- No global marked configuration for sanitization is found
- No DOMPurify or other sanitization utilities are present
- Direct use of
v-html
with unsanitized content poses XSS risksConsider implementing one of these solutions:
- Use DOMPurify to sanitize the markdown output
- Configure marked with
sanitize: true
option- Use a Vue-specific markdown component that handles sanitization
🔗 Analysis chain
Verify markdown sanitization for XSS prevention
Using
v-html
with markdown content could expose the application to XSS attacks.Consider using DOMPurify or configuring marked with proper sanitization:
import DOMPurify from 'dompurify'; const computeMarkdown = computed(() => DOMPurify.sanitize( marked.parse(writableContentItem.value?.contentItemView?.[0]?.content || "") ) );🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Check if DOMPurify or similar sanitization is used rg -l "DOMPurify|sanitize|xss" --type ts --type vue # Check marked configuration ast-grep --pattern 'marked.setOptions({ sanitize: true })'Length of output: 142
Script:
#!/bin/bash # Search for markdown-related imports and configurations rg -g "*.{js,ts,vue}" "import.*marked|marked.setOptions|DOMPurify" -A 3 # Search for computeMarkdown computed property implementation ast-grep --pattern 'computed(() => { $$$ })' # Search for sanitization-related code rg -g "*.{js,ts,vue}" "sanitize|xss|purify" -iLength of output: 4755
Script:
#!/bin/bash # Let's check the specific implementation of computeMarkdown in the conflict prevention component rg -g "*.vue" "computeMarkdown" -B 2 -A 5 "frontend/src/features/admin/features/the-additional/features/the-conflict-prevention/the-conflict-prevention.vue" # Check if marked is configured anywhere in the codebase rg -g "*.{js,ts,vue}" "marked\.(setup|setOptions|use)" -B 2 -A 2 # Look for any markdown-related utility or helper files fd -g "*mark*.{js,ts,vue}"Length of output: 1214
Script:
#!/bin/bash # Let's check the content of the MarkDownAlert component for any sanitization logic cat "frontend/src/features/admin/features/commons/MarkDownAlert.vue" # Check if there's any global marked configuration in main files rg -g "*.{js,ts}" "marked" "frontend/src/main.ts" "frontend/src/app.ts" "frontend/src/plugins/*" # Look for any security-related utilities fd -g "*security*.{js,ts}" -g "*sanitize*.{js,ts}"Length of output: 985
frontend/src/features/admin/features/the-contact-points/components/EditContactPoint.vue (2)
447-462: 🛠️ Refactor suggestion
Enhance error handling
Consider improving error handling by:
- Using specific error types for different failure scenarios
- Adding retry mechanisms for transient failures
- Providing more detailed error messages
Example implementation:
type ContactPointError = { type: 'READ' | 'WRITE' | 'VALIDATION'; message: string; retryable: boolean; }; const error = ref<ContactPointError | null>(null); const retryCount = ref(0); const handleError = (type: ContactPointError['type'], message: string) => { error.value = { type, message, retryable: type === 'READ' || type === 'WRITE' }; }; const retry = async () => { if (error.value?.retryable && retryCount.value < 3) { retryCount.value++; // Retry the failed operation } };
418-418:
⚠️ Potential issueRemove incorrect console error import
The import of
error
from 'console' is problematic as it shadows the local error function defined in the component. This could lead to unexpected behavior.- import { error } from "console";
frontend/src/features/admin/the-admin-overview.vue (1)
87-89:
⚠️ Potential issueAvoid Mutating Imported Route Objects
Directly modifying the
path
property of imported route objects (adminContactPointsRoutes
andadminOffersRoutes
) can lead to unintended side effects elsewhere in the application, as these objects may be used in other components or contexts. It's a best practice to avoid mutating imported objects to maintain predictable behavior.Consider cloning the route objects before modifying them:
87 get anlaufstellen(): unknown { -88 adminContactPointsRoutes.path = "/admin/anlaufstellen/"; -89 return adminContactPointsRoutes; +88 return { ...adminContactPointsRoutes, path: "/admin/anlaufstellen/" }; 89 } 96 get angebote(): unknown { -97 adminOffersRoutes.path = "/admin/angebote/"; -98 return adminOffersRoutes; +97 return { ...adminOffersRoutes, path: "/admin/angebote/" }; 98 }Also applies to: 96-98
frontend/src/features/admin/features/the-offers/the-offers-overview.vue (2)
146-147:
⚠️ Potential issueFix the incorrect condition in
handleIdChange
The condition in the
if
statement seems malformed and may lead to unexpected behavior.if (!item && !item == undefined && item == null) {This condition is confusing and redundant. A simplified and correct condition would be:
if (!item) {Apply this diff to correct the condition:
- if (!item && !item == undefined && item == null) { + if (!item) {
261-277: 🛠️ Refactor suggestion
Update deprecated
::v-deep
selectors in stylesThe
::v-deep
combinator is deprecated in Vue 3. The recommended syntax is now using the:deep()
pseudo-class.Apply this diff to update the styles:
-::v-deep textarea::-webkit-scrollbar { +:deep(textarea)::-webkit-scrollbar { width: 20px; } -::v-deep textarea::-webkit-scrollbar-track { +:deep(textarea)::-webkit-scrollbar-track { background-color: transparent; } -::v-deep textarea::-webkit-scrollbar-thumb { +:deep(textarea)::-webkit-scrollbar-thumb { background-color: #d6dee1; border-radius: 20px; border: 6px solid transparent; background-clip: content-box; } -::v-deep textarea::-webkit-scrollbar-thumb:hover { +:deep(textarea)::-webkit-scrollbar-thumb:hover { background-color: #a8bbbf; }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.:deep(textarea)::-webkit-scrollbar { width: 20px; } :deep(textarea)::-webkit-scrollbar-track { background-color: transparent; } :deep(textarea)::-webkit-scrollbar-thumb { background-color: #d6dee1; border-radius: 20px; border: 6px solid transparent; background-clip: content-box; } :deep(textarea)::-webkit-scrollbar-thumb:hover { background-color: #a8bbbf;
frontend/src/features/admin/features/the-offers/components/NewOffer.vue (5)
280-280:
⚠️ Potential issueRemove unnecessary import of 'error' from 'console'
Importing
error
from"console"
is unnecessary and may conflict with the locally definederror
method. Please remove this import statement.Apply this diff:
- import { error } from "console";
382-382:
⚠️ Potential issueSanitize user input to prevent XSS vulnerabilities
The
computeMarkdown
computed property renders user input without sanitization. This could lead to XSS attacks. Please sanitize the output using a library likeDOMPurify
.Apply this diff:
+ import DOMPurify from 'dompurify'; - const computeMarkdown = computed(() => marked.parse(newOffer.value?.description || "")); + const computeMarkdown = computed(() => DOMPurify.sanitize(marked.parse(newOffer.value?.description || "")));Committable suggestion skipped: line range outside the PR's diff.
453-462: 🛠️ Refactor suggestion
Avoid direct DOM manipulation; use refs instead
The
changeDescription
method directly accesses the DOM usingdocument.querySelector
, which is against Vue's best practices. Use Vue refs to interact with DOM elements reactively.Apply this diff:
+ const descriptionTextarea = ref<HTMLTextAreaElement | null>(null); const changeDescription = () => { - const textarea = document.querySelector('#description-textarea') as HTMLTextAreaElement; - const value = textarea.value; + const value = descriptionTextarea.value?.value || ''; if (newOffer.value) { newOffer.value = { ...newOffer.value, description: value }; } };In the template:
<v-textarea id="description-textarea" + ref="descriptionTextarea" color="secondary"
Committable suggestion skipped: line range outside the PR's diff.
315-324:
⚠️ Potential issueReview the logic in the 'watch' function for 'newOffer'
The
watch
function onnewOffer
appears to be unnecessary and could lead to unintended behavior. SincenewOffer
is aref
, changes tonewOffer.value
are reactive. Consider removing or revising this watcher.Apply this diff to remove the watcher:
- watch(newOffer, (newValue) => { - if (!newOffer.value) { - newOffer.value = newValue; - if (newValue) { - displayStartDate.value = newValue.startDate ? formatDateForDisplay(newValue.startDate) : ''; - displayEndDate.value = newValue.endDate ? formatDateForDisplay(newValue.endDate) : ''; - } - } - });
465-521:
⚠️ Potential issueUse Markdown syntax in 'applyFormatting' instead of HTML tags
The
applyFormatting
method inserts HTML tags into the description, but since you're using themarked
parser, it's better to insert Markdown syntax. This ensures proper rendering and reduces XSS risks.Apply this diff to update formatting:
switch (format) { case 'bold': - newText = `<b>${selectedText}</b>`; + newText = `**${selectedText}**`; break; case 'italic': - newText = `<i>${selectedText}</i>`; + newText = `*${selectedText}*`; break; case 'underline': - newText = `<u>${selectedText}</u>`; + newText = `<ins>${selectedText}</ins>`; break; case 'ordered-list': - newText = `\n<ol>\n<li>${selectedText}</li>\n<li></li>\n<li></li>\n</ol>\n`; + newText = `\n1. ${selectedText}\n2. \n3. \n`; break; case 'unordered-list': - newText = `\n<ul>\n<li>${selectedText}</li>\n<li></li>\n<li></li>\n</ul>\n`; + newText = `\n- ${selectedText}\n- \n- \n`; break; case 'line-break': - newText = '<br />'; + newText = ' \n'; break; case 'h1': - newText = `<h1>${selectedText}</h1>`; + newText = `# ${selectedText}`; break; case 'h2': - newText = `<h2>${selectedText}</h2>`; + newText = `## ${selectedText}`; break; case 'h3': - newText = `<h3>${selectedText}</h3>`; + newText = `### ${selectedText}`; break; case 'h4': - newText = `<h4>${selectedText}</h4>`; + newText = `#### ${selectedText}`; break; case 'h5': - newText = `<h5>${selectedText}</h5>`; + newText = `##### ${selectedText}`; break; default: newText = selectedText; break; }Additionally, update the
computeMarkdown
to sanitize the parsed Markdown as mentioned earlier.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.const applyFormatting = (format: string) => { const textarea = document.querySelector('#description-textarea') as HTMLTextAreaElement | null; if (!textarea) return; const start = textarea.selectionStart; const end = textarea.selectionEnd; const selectedText = textarea.value.substring(start, end); let newText = ''; switch (format) { case 'bold': newText = `**${selectedText}**`; break; case 'italic': newText = `*${selectedText}*`; break; case 'underline': newText = `<ins>${selectedText}</ins>`; break; case 'ordered-list': newText = `\n1. ${selectedText}\n2. \n3. \n`; break; case 'unordered-list': newText = `\n- ${selectedText}\n- \n- \n`; break; case 'line-break': newText = ' \n'; break; case 'h1': newText = `# ${selectedText}`; break; case 'h2': newText = `## ${selectedText}`; break; case 'h3': newText = `### ${selectedText}`; break; case 'h4': newText = `#### ${selectedText}`; break; case 'h5': newText = `##### ${selectedText}`; break; default: newText = selectedText; break; } const currentValue = newOffer.value?.description || ''; const newValue = currentValue.substring(0, start) + newText + currentValue.substring(end); newOffer.value = { ...newOffer.value, description: newValue } as Offer; };
frontend/src/features/admin/features/the-offers/components/EditOffer.vue (5)
425-425:
⚠️ Potential issueSanitize markdown content to prevent XSS vulnerabilities
Rendering user-generated markdown without sanitization can expose your application to cross-site scripting (XSS) attacks. Since
marked
no longer includes built-in sanitization, it's important to sanitize the output before rendering.Integrate a sanitizer like
DOMPurify
to sanitize the parsed markdown.
- Install
DOMPurify
:npm install dompurify
- Import
DOMPurify
in your script:+import DOMPurify from 'dompurify';
- Update
computeMarkdown
to sanitize the HTML:const computeMarkdown = computed(() => { const rawHtml = marked.parse(writableOffer.value?.description || ""); + const cleanHtml = DOMPurify.sanitize(rawHtml); + return cleanHtml; - return rawHtml; });This ensures that any malicious scripts are removed before rendering the content.
310-311:
⚠️ Potential issueRemove unnecessary import of
error
from"console"
The import of
error
from"console"
is unnecessary and may cause confusion. Theconsole
object is globally available in JavaScript, so you can useconsole.error
directly without importing it.Apply this diff to remove the unnecessary import:
-import { error } from "console";
Ensure that any references to
error
in your code are correctly handled.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.import { VContainer, VCard, VCardTitle, VCardText, VForm, VRow, VCol, VTextField, VMenu, VDatePicker, VDivider, VBtn, VIcon, VTextarea, VFileInput, VCardActions } from "vuetify/lib";
498-507: 🛠️ Refactor suggestion
Avoid direct DOM manipulation in
changeDescription
functionAccessing the DOM directly using
document.querySelector
is discouraged in Vue.js. Instead, use aref
to reference the textarea and maintain reactivity.Refactor your code to use a
ref
:Template:
<v-textarea id="description-textarea" + ref="descriptionTextarea" v-model="writableOffer.description" ... />
Script:
+const descriptionTextarea = ref<HTMLTextAreaElement | null>(null); const changeDescription = () => { - const textarea = document.querySelector('#description-textarea') as HTMLTextAreaElement; + const textarea = descriptionTextarea.value; if (!textarea) return; const value = textarea.value; if (writableOffer.value) { writableOffer.value = { ...writableOffer.value, description: value }; } };Make sure to include
descriptionTextarea
in your returned data:return { ..., + descriptionTextarea, ... }
Committable suggestion skipped: line range outside the PR's diff.
383-415:
⚠️ Potential issueCorrect validation rules in
validateDateRange
andvalidateDateFields
The computed properties
validateDateRange
andvalidateDateFields
are not returning functions as required by Vuetify's validation rules. Instead, they return objects or booleans, which may cause the validation to not work as expected.Refactor the validation computed properties to return functions that accept the value and return
true
or an error message.Example refactor for
validateDateRange
:-const validateDateRange = computed(() => { - const startDate = writableOffer.value?.startDate; - const endDate = writableOffer.value?.endDate; - - if (startDate && endDate) { - const formattedStartDate = formatDateString(startDate); - const formattedEndDate = formatDateString(endDate); - - const startDateObj = new Date(formattedStartDate); - const endDateObj = new Date(formattedEndDate); - - if (startDateObj > endDateObj) { - return { - startDate: 'Startdatum kann nicht größer sein als Enddatum', - endDate: 'Enddatum kann nicht kleiner als Startdatum sein', - }; - } - } - return true; -}); +const validateStartDateRange = (value: string) => { + const startDate = writableOffer.value?.startDate; + const endDate = writableOffer.value?.endDate; + if (startDate && endDate) { + const startDateObj = new Date(formatDateString(startDate)); + const endDateObj = new Date(formatDateString(endDate)); + if (startDateObj > endDateObj) { + return 'Startdatum kann nicht größer sein als Enddatum'; + } + } + return true; +}; + +const validateEndDateRange = (value: string) => { + const startDate = writableOffer.value?.startDate; + const endDate = writableOffer.value?.endDate; + if (startDate && endDate) { + const startDateObj = new Date(formatDateString(startDate)); + const endDateObj = new Date(formatDateString(endDate)); + if (startDateObj > endDateObj) { + return 'Enddatum kann nicht kleiner als Startdatum sein'; + } + } + return true; +};Similarly, adjust
validateDateFields
:-const validateDateFields = computed(() => { - const startDate = writableOffer.value?.startDate; - const endDate = writableOffer.value?.endDate; - - if ((startDate && !endDate) || (!startDate && endDate)) { - return 'Beide Felder müssen ausgefüllt werden, oder beide können gelöscht werden'; - } - return true; -}); +const validateDateFields = (value: string) => { + const startDate = writableOffer.value?.startDate; + const endDate = writableOffer.value?.endDate; + if ((startDate && !endDate) || (!startDate && endDate)) { + return 'Beide Felder müssen ausgefüllt werden, oder beide können gelöscht werden'; + } + return true; +};Update the validation rules in your template:
<v-text-field v-model="writableOffer.startDate" color="secondary" label="Startdatum" - :rules="[validateDateRange.startDate, validateDateFields]" + :rules="[validateStartDateRange, validateDateFields]" prepend-icon="mdi-calendar" clearable v-bind="attrs" v-on="on" /> <v-text-field v-model="writableOffer.endDate" color="secondary" label="Enddatum" - :rules="[validateDateRange.endDate, validateDateFields]" + :rules="[validateEndDateRange, validateDateFields]" prepend-icon="mdi-calendar" clearable v-bind="attrs" v-on="on" />📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.const validateStartDateRange = (value: string) => { const startDate = writableOffer.value?.startDate; const endDate = writableOffer.value?.endDate; if (startDate && endDate) { const startDateObj = new Date(formatDateString(startDate)); const endDateObj = new Date(formatDateString(endDate)); if (startDateObj > endDateObj) { return 'Startdatum kann nicht größer sein als Enddatum'; } } return true; }; const validateEndDateRange = (value: string) => { const startDate = writableOffer.value?.startDate; const endDate = writableOffer.value?.endDate; if (startDate && endDate) { const startDateObj = new Date(formatDateString(startDate)); const endDateObj = new Date(formatDateString(endDate)); if (startDateObj > endDateObj) { return 'Enddatum kann nicht kleiner als Startdatum sein'; } } return true; }; const validateDateFields = (value: string) => { const startDate = writableOffer.value?.startDate; const endDate = writableOffer.value?.endDate; if ((startDate && !endDate) || (!startDate && endDate)) { return 'Beide Felder müssen ausgefüllt werden, oder beide können gelöscht werden'; } return true; };
512-567: 🛠️ Refactor suggestion
Use Markdown syntax in
applyFormatting
functionThe
applyFormatting
function inserts HTML tags directly, but since you're usingmarked
to parse Markdown, it's better to use Markdown syntax for consistency and easier maintenance.Update the
applyFormatting
function to use Markdown syntax:switch (format) { case 'bold': - newText = `<b>${selectedText}</b>`; + newText = `**${selectedText}**`; break; case 'italic': - newText = `<i>${selectedText}</i>`; + newText = `*${selectedText}*`; break; case 'underline': - newText = `<u>${selectedText}</u>`; + newText = `<u>${selectedText}</u>`; // Markdown doesn't support underline, so HTML is acceptable here break; case 'ordered-list': - newText = `\n<ol>\n<li>${selectedText}</li>\n<li></li>\n<li></li>\n</ol>\n`; + newText = `\n1. ${selectedText}\n2. \n3. \n`; break; case 'unordered-list': - newText = `\n<ul>\n<li>${selectedText}</li>\n<li></li>\n<li></li>\n</ul>\n`; + newText = `\n- ${selectedText}\n- \n- \n`; break; case 'line-break': - newText = '<br />'; + newText = `\n`; break; case 'h1': - newText = `<h1>${selectedText}</h1>`; + newText = `# ${selectedText}`; break; case 'h2': - newText = `<h2>${selectedText}</h2>`; + newText = `## ${selectedText}`; break; case 'h3': - newText = `<h3>${selectedText}</h3>`; + newText = `### ${selectedText}`; break; case 'h4': - newText = `<h4>${selectedText}</h4>`; + newText = `#### ${selectedText}`; break; case 'h5': - newText = `<h5>${selectedText}</h5>`; + newText = `##### ${selectedText}`; break; default: newText = selectedText; break; }Also, utilize the
descriptionTextarea
ref to avoid direct DOM manipulation:const applyFormatting = (format: string) => { - const textarea = document.querySelector('#description-textarea') as HTMLTextAreaElement | null; + const textarea = descriptionTextarea.value; if (!textarea) return; const start = textarea.selectionStart; const end = textarea.selectionEnd; // ... rest of the function };📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.const applyFormatting = (format: string) => { const textarea = descriptionTextarea.value; if (!textarea) return; const start = textarea.selectionStart; const end = textarea.selectionEnd; const selectedText = textarea.value.substring(start, end); let newText = ''; switch (format) { case 'bold': newText = `**${selectedText}**`; break; case 'italic': newText = `*${selectedText}*`; break; case 'underline': newText = `<u>${selectedText}</u>`; // Markdown doesn't support underline, so HTML is acceptable here break; case 'ordered-list': newText = `\n1. ${selectedText}\n2. \n3. \n`; break; case 'unordered-list': newText = `\n- ${selectedText}\n- \n- \n`; break; case 'line-break': newText = `\n`; break; case 'h1': newText = `# ${selectedText}`; break; case 'h2': newText = `## ${selectedText}`; break; case 'h3': newText = `### ${selectedText}`; break; case 'h4': newText = `#### ${selectedText}`; break; case 'h5': newText = `##### ${selectedText}`; break; default: newText = selectedText; break; } const currentValue = writableOffer.value?.description || ''; const newValue = currentValue.substring(0, start) + newText + currentValue.substring(end); writableOffer.value = { ...writableOffer.value, description: newValue } as Offer; };
Description
New Feature Offers
Moved Admin Info Button
Refactored
Reference
Issues #XXX
Summary by CodeRabbit
Release Notes
New Features
NewOffer
,EditOffer
, andTheOffer
.BackButton
component to various pages for improved navigation.useGetOfferListItems
,useGetEditableOffers
, anduseGetOffer
.Improvements
Bug Fixes
Chores